package edu.mit.csail.relo.jdt.builder.asm;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.eclipse.core.resources.IResource;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.openrdf.model.Resource;

import edu.mit.csail.relo.ReloCore;
import edu.mit.csail.relo.jdt.RJCore;
import edu.mit.csail.relo.jdt.ReloJDTPlugin;
import edu.mit.csail.relo.jdt.extractors.MethodParametersExtractor;
import edu.mit.csail.relo.store.ReloRdfRepository;

/**
 * This is the root visitor for parsing Java Class files to RDF statements used
 * by Relo. Relevant class data is parsed and added to the Relo RDF database.
 * 
 * @author Cyrus J. Kalbrener
 * @version 1.0
 */
public class AsmClassVisitor implements ClassVisitor {

	private static final Logger logger = ReloJDTPlugin.getLogger(AsmClassVisitor.class);

	private static final Resource emptyResource = ReloCore.createReloURI("asm#NONE");

	static final String OBJECT_TYPE_NAME = Type.getType(Object.class).getInternalName();

	private static final int UNKNOWN_ACCESS = 65536;

	private static Map<String, Resource> packages = new LinkedHashMap<String, Resource>();

	private IResource eclipseClassResource;
	private Resource projectRes;
	private ReloRdfRepository reloRdf;
	private Resource classKey;
	private Resource sourceResource = emptyResource;
	private String classLabel;
	private String fqClassname;
	private String rdfClassname;

    private final String projName;

	public AsmClassVisitor(ReloRdfRepository rrr, Resource projectRes, String projName, IResource eclipseClassResource) {
		this.reloRdf = rrr;
		this.projectRes = projectRes;
        this.projName = projName;
		this.eclipseClassResource = eclipseClassResource;
	}
    
    public Resource getClassRes() {
        return classKey;
    }

	public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
		Type classType = AsmUtil.internalNameToType(name);
		//if (signature != null)
		//	new SignatureReader(signature).accept(new AsmSignatureVisitor(reloRdf, classKey));
		this.fqClassname = name;
		this.classLabel = name.substring(name.lastIndexOf('/') + 1);
		this.rdfClassname = AsmUtil.getRdfClassName(name);
		this.classKey = addClassOrInterface(classType, access, projectRes);
		reloRdf.addStatement(classKey, ReloCore.name, classLabel);
		addInheritance(superName, UNKNOWN_ACCESS, projectRes);

		for (String iface : interfaces) {
			addInheritance(iface, UNKNOWN_ACCESS + Opcodes.ACC_INTERFACE, projectRes);
		}
	}


	public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
		return new AsmAnnotationVisitor(reloRdf);
	}


	public void visitAttribute(Attribute attr) {
	}


	public void visitEnd() {
	}


	public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
		//if (desc != null)
		//	new SignatureReader(desc).accept(new AsmSignatureVisitor(reloRdf, classKey));
		//if (signature != null)
		//	new SignatureReader(desc).accept(new AsmSignatureVisitor(reloRdf, classKey));
		Resource fieldKey = AsmUtil.toReloResource(reloRdf, rdfClassname + "." + name);
		reloRdf.addStatement(classKey, ReloCore.contains, fieldKey);
		reloRdf.addStatement(fieldKey, ReloCore.name, name);
		reloRdf.addStatement(fieldKey, RJCore.access, AsmUtil.getAccessModifierResource(access));

		Type fieldType = Type.getType(desc);
		Resource fieldResource = AsmUtil.toReloClassResource(reloRdf, fieldType);
		reloRdf.addStatement(fieldKey, RJCore.refType, fieldResource);
		reloRdf.addStatement(fieldKey, RJCore.srcResource, sourceResource);
		reloRdf.addStatement(fieldKey, reloRdf.rdfType, RJCore.fieldType);
		reloRdf.addStatement(fieldKey, ReloCore.initialized, true);

		return null;
	}

	public void visitInnerClass(String name, String outerName, String innerName, int access) {
		if (outerName == null)
			outerName = name.substring(0, name.lastIndexOf("$"));

		Type outerType = AsmUtil.internalNameToType(outerName);
		Type innerType = AsmUtil.internalNameToType(name);

		reloRdf.addStatement(AsmUtil.toReloClassResource(reloRdf, outerType), ReloCore.contains, AsmUtil.toReloClassResource(reloRdf, innerType));
	}

	public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
		if (name.equals("<clinit>")) {
			return new AsmMethodVisitor(reloRdf, classKey);
		}
		
		StringBuilder buff = new StringBuilder(rdfClassname).append('.');

		String displayName = name;
		boolean anonymousMethod = false;
		if (displayName.equals("<init>")) displayName = classLabel;
		if (displayName.contains("$")) {
			anonymousMethod = true;
			displayName = displayName.substring(displayName.lastIndexOf('$') + 1);
		}
		
		buff.append(AsmUtil.getMethodSignature(displayName, desc));

		Resource methodKey = AsmUtil.toReloResource(reloRdf, buff.toString());

		//new SignatureReader(desc).accept(new AsmSignatureVisitor(reloRdf, methodKey));

		reloRdf.addStatement(classKey, ReloCore.contains, methodKey);
		reloRdf.addStatement(methodKey, ReloCore.name, displayName);
		reloRdf.addStatement(methodKey, RJCore.access, AsmUtil.getAccessModifierResource(access));
		reloRdf.addStatement(methodKey, reloRdf.rdfType, RJCore.methodType);
		reloRdf.addStatement(methodKey, MethodParametersExtractor.parameterCachedLabel, AsmUtil.getMethodSignature(null, desc));
		reloRdf.addStatement(methodKey, RJCore.srcResource, sourceResource);
		if (anonymousMethod) {
			reloRdf.addStatement(methodKey, AsmMethodVisitor.anonymousMethodType, true);
		}

		Type[] types = Type.getArgumentTypes(desc);

		if (types.length > 0) {
			List<Resource> params = new ArrayList<Resource>(types.length);

			for (Type t : types) {
				Resource paramResource = AsmUtil.toReloClassResource(reloRdf, t);
				reloRdf.addStatement(methodKey, RJCore.refType, paramResource);
				params.add(paramResource);
			}

			reloRdf.addStatement(methodKey, RJCore.parameter, reloRdf.createList(params));

			Resource retTypeResource = AsmUtil.toReloClassResource(reloRdf, Type.getReturnType(desc));
			reloRdf.addStatement(methodKey, RJCore.refType, retTypeResource);
			reloRdf.addStatement(methodKey, RJCore.returnType, retTypeResource);
		}

		return new AsmMethodVisitor(reloRdf, methodKey);
	}

	public void visitOuterClass(String owner, String name, String desc) {
		// For some reason this guy never gets called in pre-1.5 classes.
		// LOG.info("visitOuterClass() owner=" + owner + ", name=" + name + ",
		// desc=" + desc);
	}

	/**
	 * Tries to use the ICompilationUnit if availible, otherwise creates a fake
	 * entry based upon the classname and package path.
	 * 
	 * @param source
	 * @param debug
	 */
	public void visitSource(String source, String debug) {
		if (eclipseClassResource == null) {
			sourceResource = getEncodedResource(source);
			reloRdf.addStatement(sourceResource, ReloCore.name, 
					fqClassname.substring(0, fqClassname.lastIndexOf('/') + 1) + source);
		} else {
			sourceResource = ReloCore.eclipseResourceToRDFResource(reloRdf,eclipseClassResource);
			reloRdf.addStatement(sourceResource, ReloCore.name, eclipseClassResource.getName());
		}

		if (sourceResource == null) {
			logger.error("Source Resource for " + this.classLabel + " is NULL");
		}

		reloRdf.addStatement(classKey, RJCore.srcResource, sourceResource);
	}

	private Resource addClassOrInterface(Type type, int access, Resource projectResource) {
		Resource classKey = AsmUtil.getClassKey(type.getClassName(), reloRdf);
		if (AsmUtil.isPrimitive(type)) {
			return classKey;
		}

		addPackage(type, classKey, projectResource);

		if ((access & Opcodes.ACC_INTERFACE) == Opcodes.ACC_INTERFACE) {
			reloRdf.addStatement(classKey, reloRdf.rdfType, RJCore.interfaceType);
		} else if ((access & UNKNOWN_ACCESS) == UNKNOWN_ACCESS) {
			// do nothing
		} else {
			reloRdf.addStatement(classKey, reloRdf.rdfType, RJCore.classType);
		}

		if ((access & UNKNOWN_ACCESS) == 0) {
			reloRdf.addStatement(classKey, RJCore.access, AsmUtil.getAccessModifierResource(access));
		}

		reloRdf.addStatement(classKey, ReloCore.name, AsmUtil.getShortClassname(type));
		reloRdf.addStatement(classKey, ReloCore.initialized, true);

		return classKey;
	}

	private void addInheritance(String superName, int access,
			Resource projectResource) {
		if ((superName == null) || superName.equals(OBJECT_TYPE_NAME)) {
			return;
		}

		Resource superResource = AsmUtil.toReloClassResource(reloRdf, AsmUtil.internalNameToType(superName));
		reloRdf.addStatement(classKey, RJCore.inherits, superResource);
		addPackage(superName, superResource, projectResource);
	}

	private Resource addPackage(Type classType, Resource classKey, Resource projectResource) {
		if (AsmUtil.isPrimitive(classType)) return emptyResource;
		if (classKey == emptyResource) return emptyResource;
		if (classType.getClassName().contains("$")) return emptyResource;

		String packageName = classType.getClassName();
		int packageNdx = packageName.lastIndexOf('.');
		if (packageNdx == -1)
			packageName = "(default)";
		else
			packageName = packageName.substring(0, packageNdx);

		Resource packageKey = packages.get(packageName);

		if (packageKey == null) {
			packageKey = AsmUtil.toReloResource(reloRdf, packageName);
			packages.put(packageName, packageKey);
			reloRdf.addStatement(projectResource, ReloCore.contains, packageKey);
			reloRdf.addStatement(packageKey, ReloCore.name, packageName);
			reloRdf.addStatement(packageKey, reloRdf.rdfType, RJCore.packageType);
			reloRdf.addStatement(packageKey, ReloCore.initialized, true);
		}

		reloRdf.addStatement(packageKey, ReloCore.contains, classKey);

		return packageKey;
	}

	private Resource addPackage(String className, Resource classKey, Resource projectResource) {
		return this.addPackage(AsmUtil.internalNameToType(className), classKey, projectResource);
	}

	private Resource getEncodedResource(String s) {
		StringBuilder buff = new StringBuilder(100);
		buff.append(RJCore.jdtWkspcNS).append("/").append(projName);

		if (s.length() > 0) {
			buff.append("/").append(s);
		}

		return ReloCore.createReloURI(ReloRdfRepository.reloRdfNamespace
				+ ReloCore.encodeId(s));
	}
}
